﻿using System;
using System.Collections.Generic;
using System.IO;

namespace MyLib.IO {
	/// <summary>
	/// File system provider.
	/// </summary>
	public sealed class FileSystemProvider : DbContextProvider {
		/// <summary>
		/// Type of temporary files.
		/// </summary>
		public const int TEMP_FILE = 0;

		/// <summary>
		/// 
		/// </summary>
		private static IDictionary<int, string> m_folders;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="subFolder"></param>
		public static void RegisterFileType(int fileType, string subFolder) {
			if (m_folders == null)
				m_folders = new Dictionary<int, string>();

			System.Diagnostics.Debug.Assert(fileType > 0);
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(subFolder));
			System.Diagnostics.Debug.Assert(!m_folders.ContainsKey(fileType));

			m_folders.Add(fileType, subFolder);
		}

		/// <summary>
		/// 
		/// </summary>
		internal static void UnregisterFileTypes() {
			if (m_folders != null)
				m_folders.Clear();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="types"></param>
		public static void RegisterFileTypes(IDictionary<int, string> types) {
			System.Diagnostics.Debug.Assert(types != null);

			foreach (var entry in types) {
				RegisterFileType(entry.Key, entry.Value);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <returns></returns>
		public IEnumerable<FileObject> GetFileObjects(int fileType, string objectId) {
			if (fileType != TEMP_FILE && !m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));

			var parameters = new DbParameter[2];
			parameters[0] = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			parameters[1] = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);

			using (var session = SessionFactory.CreateReadOnlySession()) {
				var reader = session.ExecuteReader(
				"select file_id, `name`, file_type, `object_id`, `size` from fs_files where file_type = #file_type# and `object_id` = #object_id#",
					System.Data.CommandType.Text,
					parameters);

				while (reader.Read()) {
					yield return new FileObject(
						reader.GetString(0),
						reader.GetString(1),
						reader.GetInt32(2),
						reader.GetString(3),
						Convert.ToInt64(reader.GetValue(4))
						);
				} // end of while.

				reader.Close();
			} // end of using.
		}

		/// <summary>
		/// Move files.
		/// </summary>
		/// <param name="srcFileType"></param>
		/// <param name="srcObjectId"></param>
		/// <param name="dstFileType"></param>
		/// <param name="dstObjectId"></param>
		/// <returns></returns>
		public int MoveFileObjects(int srcFileType, string srcObjectId, int dstFileType, string dstObjectId) {
			if (srcFileType != TEMP_FILE && !m_folders.ContainsKey(srcFileType))
				throw new ArgumentOutOfRangeException("srcFileType", srcFileType,
					string.Format("FileType {0} has not been registerd.", srcFileType));

			if (!m_folders.ContainsKey(dstFileType))
				throw new ArgumentOutOfRangeException("dstFileType", dstFileType,
					string.Format("FileType {0} has not been registerd.", dstFileType));

			var parameters = new DbParameter[4];
			parameters[0] = new DbParameter("src_file_type", MyLib.DbParameterType.Integer, srcFileType);
			parameters[1] = new DbParameter("src_object_id", MyLib.DbParameterType.String, srcObjectId);
			parameters[2] = new DbParameter("dst_file_type", MyLib.DbParameterType.Integer, dstFileType);
			parameters[3] = new DbParameter("dst_object_id", MyLib.DbParameterType.String, dstObjectId);

			using (var session = SessionFactory.CreateSession()) {
				var ret = session.ExecuteUpdate(
				"update fs_files set file_type = #dst_file_type#, `object_id` = #dst_object_id# where file_type = #src_file_type# and `object_id` = #src_object_id#",
					System.Data.CommandType.Text,
					parameters);

				session.Commit();

				return ret;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileId"></param>
		/// <returns></returns>
		public FileObject GetFileObject(string fileId) {
			if (string.IsNullOrWhiteSpace(fileId))
				throw new ArgumentException("Parameter \"fileId\" cannot be null or empty.", "fileId");

			var parameters = new DbParameter[1];
			parameters[0] = new DbParameter("file_id", MyLib.DbParameterType.String, fileId);

			using (var session = SessionFactory.CreateReadOnlySession()) {
				var reader = session.ExecuteReader(
					"select file_id, `name`, file_type, `object_id`, `size`, `description`, `summary` from fs_files where file_id = #file_id#",
					System.Data.CommandType.Text,
					parameters);

				FileObject fo = null;
				if (reader.Read()) {
					fo = new FileObject(
						reader.GetString(0),
						reader.GetString(1),
						reader.GetInt32(2),
						reader.GetString(3),
						Convert.ToInt64(reader.GetValue(4)),
						reader.GetString(5),
						reader.GetString(6)
						);
				}

				reader.Close();

				if (fo != null) {
					reader = session.ExecuteReader(
						"select `content` from fs_files where file_id = #file_id#",
						System.Data.CommandType.Text,
						parameters);

					if (reader.Read()) {
						fo.Content = GetBytes(reader, 0);
					}

					reader.Close();
				}
				return fo;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="fileName"></param>
		/// <returns></returns>
		public FileObject GetFileObject(int fileType, string objectId, string fileName) {
			if (fileType != TEMP_FILE && !m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");
			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentException("Parameter \"fileName\" cannot be null or empty.", "fileName");

			var parameters = new DbParameter[3];
			parameters[0] = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			parameters[1] = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			parameters[2] = new DbParameter("name", MyLib.DbParameterType.String, fileName);

			using (var session = SessionFactory.CreateReadOnlySession()) {
				FileObject fo = null;

				using (var reader = session.ExecuteReader(
					"select file_id, `name`, file_type, `object_id`, `size`, `description`, `summary` from fs_files where file_type = #file_type# and object_id = #object_id# and `name` = #name#",
					System.Data.CommandType.Text,
					parameters)) {

					if (reader.Read()) {
						fo = new FileObject(
							reader.GetString(0),
							reader.GetString(1),
							reader.GetInt32(2),
							reader.GetString(3),
							Convert.ToInt64(reader.GetValue(4)),
							reader.GetString(5),
							reader.GetString(6)
							);
					}

					reader.Close();
				}

				if (fo != null) {
					using (var reader = session.ExecuteReader(
						"select `content` from fs_files where file_type = #file_type# and object_id = #object_id# and `name` = #name#",
						System.Data.CommandType.Text,
						parameters)) {

						if (reader.Read()) {
							fo.Content = GetBytes(reader, 0);
						}

						reader.Close();
					}
				}
				return fo;
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="fileName"></param>
		/// <param name="content"></param>
		/// <param name="description"></param>
		/// <param name="summary"></param>
		public void SetFileObject(int fileType, string objectId, string fileName, byte[] content, string description = "", string summary = "") {
			if (fileType != TEMP_FILE && !m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");
			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentException("Parameter \"fileName\" cannot be null or empty.", "fileName");

			if (description == null)
				description = "";
			if (summary == null)
				summary = "";

			var parameterFileType = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			var parameterObjectId = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			var parameterFileName = new DbParameter("name", MyLib.DbParameterType.String, fileName);
			var parameterDescription = new DbParameter("description", MyLib.DbParameterType.String, description);
			var parameterSummary = new DbParameter("summary", MyLib.DbParameterType.String, summary);

			using (var session = SessionFactory.CreateSession()) {
				var fileId = session.ExecuteScalar<string>(
					"select file_id from fs_files where file_type = #file_type# and `object_id` = #object_id# and `name` = #name#",
					System.Data.CommandType.Text,
					parameterFileType,
					parameterObjectId,
					parameterFileName);

				string sql = "";
				if (string.IsNullOrEmpty(fileId)) {
					fileId = Guid.NewGuid().ToString("N");
					sql = "insert into fs_files (file_id, file_type, `object_id`, `name`, `size`, content, `description`, `summary`) values(#file_id#, #file_type#, #object_id#, #name#, #size#, #content#, #description#, #summary#)";
				} else {
					sql = "update fs_files set file_type = #file_type#, `object_id` = #object_id#, `name` = #name#, `size` = #size#, content = #content#, `description` = #description#, `summary` = #summary# where file_id = #file_id#";
				}

				var parameterFileId = new DbParameter("file_id", MyLib.DbParameterType.String, fileId);
				var parameterSize = new DbParameter("size", MyLib.DbParameterType.Integer, content.Length);
				var parameterContent = new DbParameter("content", MyLib.DbParameterType.Binary, content);

				session.ExecuteUpdate(sql, System.Data.CommandType.Text,
					parameterFileType, parameterObjectId, parameterFileName,
					parameterFileId, parameterSize, parameterContent,
					parameterDescription, parameterSummary);

				session.Commit();
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileId"></param>
		public void DeleteFileObject(string fileId) {
			if (string.IsNullOrEmpty(fileId))
				throw new ArgumentNullException("fileId");

			var parameters = new DbParameter[1];
			parameters[0] = new DbParameter("file_id", MyLib.DbParameterType.String, fileId);

			using (var session = SessionFactory.CreateSession()) {
				var reader = session.ExecuteUpdate(
					"delete from fs_files where file_id = #file_id#",
					System.Data.CommandType.Text,
					parameters);

				session.Commit();
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		/// <param name="fileName"></param>
		public void DeleteFileObject(int fileType, string objectId, string fileName) {
			if (!m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");
			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentException("Parameter \"fileName\" cannot be null or empty.", "fileName");

			var parameters = new DbParameter[3];
			parameters[0] = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			parameters[1] = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);
			parameters[2] = new DbParameter("name", MyLib.DbParameterType.String, fileName);

			using (var session = SessionFactory.CreateSession()) {
				session.ExecuteUpdate(
					"delete from fs_files where file_type = #file_type# and `object_id` = #object_id# and `name` = #name#",
					System.Data.CommandType.Text,
					parameters);

				session.Commit();
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="fileType"></param>
		/// <param name="objectId"></param>
		public void DeleteAllFileObject(int fileType, string objectId) {
			if (!m_folders.ContainsKey(fileType))
				throw new ArgumentOutOfRangeException("fileType", fileType,
					string.Format("FileType {0} has not been registerd.", fileType));
			if (string.IsNullOrWhiteSpace(objectId))
				throw new ArgumentException("Parameter \"objectId\" cannot be null or empty.", "objectId");

			var parameters = new DbParameter[2];
			parameters[0] = new DbParameter("file_type", MyLib.DbParameterType.Integer, fileType);
			parameters[1] = new DbParameter("object_id", MyLib.DbParameterType.String, objectId);

			using (var session = SessionFactory.CreateSession()) {
				session.ExecuteUpdate(
					"delete from fs_files where file_type = #file_type# and `object_id` = #object_id#",
					System.Data.CommandType.Text,
					parameters);

				session.Commit();
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="reader"></param>
		/// <param name="index"></param>
		/// <returns></returns>
		private static byte[] GetBytes(System.Data.Common.DbDataReader reader, int index) {
			const int CHUNK_SIZE = 2 * 1024;
			byte[] buffer = new byte[CHUNK_SIZE];
			long bytesReadInTotal = 0;

			using (MemoryStream stream = new MemoryStream()) {
				long len = reader.GetBytes(index, 0, null, 0, 0);

				while (bytesReadInTotal < len) {
					var bytesRead = reader.GetBytes(index, bytesReadInTotal, buffer, 0, buffer.Length);
					bytesReadInTotal += bytesRead;

					stream.Write(buffer, 0, (int)bytesRead);
				}

				stream.Flush();
				return stream.ToArray();
			} // end of using.
		}

	} // end of FileSystemProvider.
}
